<?php

namespace app\service;

use app\common\PayStatus;
use app\model\User;
use app\model\VipOrder;
use EasyWeChat\Kernel\Exceptions\InvalidArgumentException;
use EasyWeChat\Pay\Application;
use support\Log;
use Symfony\Component\HttpClient\Exception\ClientException;
use Yurun\OAuthLogin\QQ\OpenidMode;
use Yurun\OAuthLogin\Weixin\OAuth2;
use Yurun\OAuthLogin\QQ\OAuth2 as QQOAuth2;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Util\PemUtil;

class WechatService
{
    const AUTH_TAG_LENGTH_BYTE = 16;
    const NOTIFY_URL           = WEB_URL . "/h5/payCallback/wechat/";
    protected array       $pay_config;
    protected array       $open_config;
    protected Application $app;
    protected OAuth2      $wxOAuth;

    public static ?WechatService $_instance = null;


    /**
     * 登录用的是微信开放平台 open 的账号
     */
    public function __construct()
    {
        $wechat_pay_cert  = app_path() . '/extend/cert/wechatpay/';
        $this->pay_config = [
            //'use_sandbox'           => false,// 是否使用 微信支付仿真测试系统
            'app_id'                => 'owenweb',
            'app_key'               => 'owenweb',
            'mch_id'                => 'owenweb',//1630437448
            'sub_mch_id'            => 'owenweb',//受理机构商户号1630107645
            //这个是微信商户平台获取apiclient_key.pem和apiclient_cert.pem
            //https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay3_1.shtml
            //工具生成apiclient_cert.pem https://kf.qq.com/faq/161222NneAJf161222U7fARv.html
            //-----BEGIN CERTIFICATE-----
            'certificate'           => $wechat_pay_cert . 'apiclient_cert.pem',
            //工具生成后保存本地是私钥 C:\Users\0\Downloads\WXCertUtil\cert
            //或者下载商户号平台下载-----BEGIN PRIVATE KEY-----
            'private_key'           => $wechat_pay_cert . 'apiclient_key.pem',
            //证书序列号
            'certificate_serial_no' => 'owenweb',
            //cert.pem这个得自己生成https://my.oschina.net/owenzhang24/blog/5531197
            //-----BEGIN CERTIFICATE-----
            'cert'                  => $wechat_pay_cert . 'cert.pem',
            'http'                  => [
                'base_uri' => 'https://api.mch.weixin.qq.com/',
            ],
            'notify_url'            => self::NOTIFY_URL,     // 你也可以在下单时单独设置来想覆盖它
            'secret_key'            => 'owenweb', // v3 APIv3密钥
        ];

        // 微信登录，登录用的是微信开放平台 open 的账号
        $this->open_config = [
            'app_id' => 'owenweb',
            'secret' => 'owenweb',
        ];
        $this->app         = new Application($this->pay_config);
        $this->wxOAuth     = (new OAuth2($this->open_config['app_id'], $this->open_config['secret']));
    }

    /**
     * 获取qq登录配置
     *
     * @return array
     */
    public function getQQLoginConfig(): array
    {
        //OwenWeb
        return [
            'app_id'  => 'owenweb',
            'app_key' => 'owenweb',
        ];

    }

    /**
     * 获取微信登录配置
     *
     * @return array
     */
    public function getWechatLoginConfig(): array
    {
        return [
            'app_id'  => 'owenweb',
            'app_key' => 'owenweb',
        ];
    }

    /**
     * @return WechatService|mixed
     */
    public static function instance(): ?WechatService
    {
        if (!static::$_instance) static::$_instance = new self();
        return static::$_instance;
    }

    //    ========================================= WechatLogin ========================================

    /**
     * 获取微信登录url
     *
     * @return string
     */
    public function getLoginUrl(): string
    {
        $url     = $this->wxOAuth->getAuthUrl(WEB_URL . '/web/login/wechat/');
        $session = request()->session();
        $session->set('QRCODE_WECHAT_STATE', $this->wxOAuth->state);
        return $url;
    }

    /**
     * 获取微信登录回调
     *
     * @return string
     */
    public function loginCallback(string $code, string $state)
    {
        try {
            $session      = request()->session();
            $access_token = $this->wxOAuth->getAccessToken($session->get('QRCODE_WECHAT_STATE'), $code, $state);
            $userInfo     = $this->wxOAuth->getUserInfo($access_token);
            $where[]      = ['unionid', '=', $userInfo['unionid']];
            $user         = User::where($where)->find();
            if (empty($user)) $user = User::wechat_register($userInfo);
            return $user;
        } catch (\InvalidArgumentException $exception) {
            return false;
        }
    }

    /**
     * 获取微信app登录回调
     *
     * @param string $code
     * @param string $state
     * @param string $ref
     * @param int    $bindUid
     * @return array
     * @throws \Yurun\OAuthLogin\ApiException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function appLoginCallback(string $code, string $state, string $ref = '', int $bindUid = 0)
    {
        $wechatLoginConfig = $this->getWechatLoginConfig();
        $appId             = $wechatLoginConfig['app_id'];
        $appKey            = $wechatLoginConfig['app_key'];

        $wxOAuth = (new OAuth2($appId, $appKey));
        try {
            $access_token = $wxOAuth->getAccessToken($state, $code, $state);
            $userInfo     = $wxOAuth->getUserInfo($access_token);
            $unionid      = $userInfo['unionid'] ?? '';
            if (!$unionid) return ['code' => 400, 'msg' => 'unionid不存在'];
            if ($bindUid) {
                //绑定
                $where[] = ['status', '=', 1];
                $where[] = ['unionid', '=', $unionid];
                $where[] = ['id', '<>', $bindUid];
                $user    = User::where($where)->find();
                if ($user) return ['code' => 400, 'msg' => '该微信已被其他账号绑定'];

                $where1[] = ['id', '=', $bindUid];
                $user     = User::where($where1)->find();
                if (!$user) return ['code' => 400, 'msg' => '绑定用户不存在'];

                $data = [
                    'unionid' => $unionid,
                    'u_time'  => date('Y-m-d H:i:s'),
                ];
                User::updateDataCache($where1, $data);
            } else {
                $where2[] = ['unionid', '=', $unionid];
                $user     = User::where($where2)->find();
            }
            if (empty($user) && !$bindUid) $user = User::wechat_register($userInfo, $ref);
            return ['code' => 200, 'data' => $user];
        } catch (\InvalidArgumentException $exception) {
            return ['code' => 400, 'msg' => '未知错误'];
        }
    }

    /**
     * 微信小程序微信登录
     *
     * @param string $code
     * @param string $encrypteData
     * @param string $iv
     * @param string $ref
     * @return array
     */
    public function wechatAppLogin(string $code, string $encrypteData, string $iv, string $ref): array
    {
        try {
            $wechatLoginConfig = $this->getWechatLoginConfig();
            $appId             = $wechatLoginConfig['app_id'];
            $appKey            = $wechatLoginConfig['app_key'];

            $wxOAuth       = (new OAuth2($appId, $appKey));
            $getSessionKey = $wxOAuth->getSessionKey($code);
            $userOpenId    = $wxOAuth->openid;
            if (!$userOpenId) return ['code' => 400, 'msg' => '用户的openid获取失败'];
            $where[] = ['wx_openid', '=', $userOpenId];
            $user    = User::where($where)->find();

            if (!$user) {
                $userInfo = $this->descryptData($encrypteData, $iv, $getSessionKey);
                if (!isset($userInfo['nickName'])) return ['code' => 400, 'msg' => $userInfo];
                $user = User::wechat_app_register('', $userOpenId, '', $userInfo['avatarUrl'], $userInfo['nickName'], $userInfo['gender'], $ref);
                if ($user['code'] != 200) return ['code' => 400, 'msg' => '用户注册失败' . $user['msg']];
                $user = $user['data'];
            }

            return ['code' => 200, 'data' => $user];
        } catch (\InvalidArgumentException $exception) {
            return ['code' => 400, 'msg' => '未知错误' . $exception->getMessage()];
        }
    }

    /**
     * 解密小程序 wx.getUserInfo() 敏感数据.
     *
     * @param string $encryptedData
     * @param string $iv
     * @param string $sessionKey
     *
     * @return array
     */
    public function descryptData(string $encryptedData, string $iv, string $sessionKey)
    {
        if (24 != \strlen($sessionKey)) {
            return 'sessionKey格式错误';
        }
        if (24 != \strlen($iv)) {
            return 'iv格式错误';
        }
        $aesKey    = base64_decode($sessionKey);
        $aesIV     = base64_decode($iv);
        $aesCipher = base64_decode($encryptedData);
        $result    = openssl_decrypt($aesCipher, 'AES-128-CBC', $aesKey, 1, $aesIV);
        $dataObj   = json_decode($result, true);
        if (!$dataObj) {
            return '解密数据获取失败';
        }

        return $dataObj;
    }

    /**
     * 微信小程序获取手机号
     *
     * @param string $code
     * @return array
     */
    public function wechatAppGetPhone(string $code)
    {
        try {
            $accessTokenRes = $this->getWechatAppAccessToken();
            $accessToken    = '';
            if (isset($accessTokenRes['data']['access_token'])) {
                if ($accessTokenRes['data']['access_token']) $accessToken = $accessTokenRes['data']['access_token'];
            }
            if (!$accessToken) return ['code' => 400, 'msg' => 'accessToken错误'];

            $url         = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=$accessToken";
            $data        = [
                'code' => $code,
            ];
            $res         = curlPost($url, $data);
            $phone       = '';
            $countryCode = '';
            if (isset($res['data']['phone_info']['purePhoneNumber'])) {
                if ($res['data']['phone_info']['purePhoneNumber']) {
                    $phone       = $res['data']['phone_info']['purePhoneNumber'];
                    $countryCode = $res['data']['phone_info']['countryCode'];
                }
            }
            if (!$phone) return ['code' => 400, 'msg' => '手机号获取失败'];

            $phoneData = [
                'phone'       => $phone,
                'countryCode' => $countryCode,
            ];
            return ['code' => 200, 'data' => $phoneData];
        } catch (\InvalidArgumentException $exception) {
            return ['code' => 400, 'msg' => '未知错误' . $exception->getMessage()];
        }
    }

    /**
     * 微信小程序获取AccessToken
     *
     * @param string $code
     * @return array
     */
    public function getWechatAppAccessToken()
    {
        $wechatLoginConfig = $this->getWechatLoginConfig();
        $appId             = $wechatLoginConfig['app_id'];
        $appKey            = $wechatLoginConfig['app_key'];
        $url               = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appId&secret=$appKey";

        return curlGet($url);
    }

    //    ========================================= QQLogin ========================================

    /**
     * 获取qq登录url
     *
     * @return string
     */
    public function qqLoginUrl(): string
    {
        $qqLoginConfig = $this->getQQLoginConfig();
        $appId         = $qqLoginConfig['app_id'];
        $appKey        = $qqLoginConfig['app_key'];

        $qqOAuth = new  QQOAuth2($appId, $appKey, WEB_URL . '/web/login/qqcallback/');
        $url     = $qqOAuth->getAuthUrl(WEB_URL . '/web/login/qqcallback/');
        $session = request()->session();
        $session->set('QRCODE_QQ_STATE', $qqOAuth->state);
        return $url;
    }

    /**
     * 获取qq登录回调
     *
     * @param string $token
     * @param string $ref
     * @param int    $bindUid
     * @return array
     * @throws \Yurun\OAuthLogin\ApiException
     * @throws \think\db\exception\DataNotFoundException
     * @throws \think\db\exception\DbException
     * @throws \think\db\exception\ModelNotFoundException
     */
    public function qqLoginCallback(string $token, string $ref = '', int $bindUid = 0): array
    {
        $getQQLoginConfig      = $this->getQQLoginConfig();
        $appId                 = $getQQLoginConfig['app_id'];
        $appKey                = $getQQLoginConfig['app_key'];
        $qqOAuth               = new QQOAuth2($appId, $appKey, urlencode(WEB_URL));
        $qqOAuth->isUseUnionID = true;
        $qqOAuth->openidMode   = OpenidMode::UNION_ID;
        try {
            $userInfo  = $qqOAuth->getUserInfo($token);
            $qqUnionid = $userInfo['unionid'] ?? '';
            if (!$qqUnionid) return ['code' => 400, 'msg' => 'unionid不存在'];
            if ($bindUid) {
                //绑定
                $where[] = ['status', '=', 1];
                $where[] = ['qq_unionid', '=', $qqUnionid];
                $where[] = ['id', '<>', $bindUid];
                $user    = User::where($where)->find();
                if ($user) return ['code' => 400, 'msg' => '该qq已被其他账号绑定'];

                $where1[] = ['id', '=', $bindUid];
                $user     = User::where($where1)->find();
                if (!$user) return ['code' => 400, 'msg' => '绑定用户不存在'];

                $data = [
                    'qq_unionid' => $qqUnionid,
                    'u_time'     => date('Y-m-d H:i:s'),
                ];
                User::updateDataCache($where1, $data);
            } else {
                $where2[] = ['qq_unionid', '=', $qqUnionid];
                $user     = User::where($where2)->find();
            }
            if (empty($user) && !$bindUid) $user = User::qq_register($userInfo, $ref);
            return ['code' => 200, 'data' => $user];
        } catch (\InvalidArgumentException $exception) {
            return ['code' => 400, 'msg' => '未知错误'];
        }
    }

    //    ========================================= PAY ========================================

    /**
     * Native支付 二维码扫码支付
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_4_1.shtml
     *{
     * "code_url": "weixin://wxpay/bizpayurl?pr=p4zz"
     * }
     * 此URL用于生成支付二维码，然后提供给用户扫码支付。
     * 注意：code_url并非固定值，使用时按照URL格式转成二维码即可。
     *
     * @param array $orderInfo
     * @return array
     */
    public function nativePay(array $orderInfo, string $title = WEB_TITLE): array
    {
        $config   = $this->app->getConfig();
        $data     = [
            'mchid'        => $config->get('mch_id'),
            'appid'        => $config->get('app_id'),
            'description'  => $title . '-vip购买',
            'out_trade_no' => $orderInfo['order_no'],
            'amount'       => ['total' => intval($orderInfo['money'] * 100)],
            'notify_url'   => self::NOTIFY_URL, // 支付结果通知网址，如果不设置则会使用配置里的默认地址
        ];
        $response = $this->app->getClient()->post('pay/transactions/native', [
            'json' => $data,
        ]);
        return $response->toArray();
    }

    /**
     * APP支付
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_2_1.shtml
     * {
     * "prepay_id": "wx261153585405160000"
     * }
     * 预支付交易会话标识。用于后续接口调用中使用，该值有效期为2小时
     *
     * @param array $orderInfo
     * @return string
     * @throws InvalidArgumentException
     */
    public function appPay(array $orderInfo, string $title = WEB_TITLE): string
    {
        $this->pay_config['app_id'] = 'owenweb';
        $app                        = new Application($this->pay_config);
        $config                     = $app->getConfig();
        $data                       = [
            'mchid'        => $config->get('mch_id'),
            'appid'        => $config->get('app_id'),
            'description'  => $title . '-vip购买',
            'out_trade_no' => $orderInfo['order_no'],
            'amount'       => ['total' => intval($orderInfo['money'] * 100)],
            'notify_url'   => self::NOTIFY_URL, // 支付结果通知网址，如果不设置则会使用配置里的默认地址
        ];

        $response = $this->app->getClient()->post('pay/transactions/app', [
            'json' => $data,
        ]);
        $res      = $response->toArray();
        $utils    = $this->app->getUtils();
        $res      = $utils->buildAppConfig($res['prepay_id'] ?? '', $config->get('app_id'));
        return urldecode(http_build_query($res));
    }

    /**
     * H5支付
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_3_1.shtml
     * {
     * "h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx291626300kage=2150917749"
     * }
     * h5_url为拉起微信支付收银台的中间页面，可通过访问该url来拉起微信客户端，完成支付，h5_url的有效期为5分钟。
     *
     * @param array $orderInfo
     * @return string
     * @throws InvalidArgumentException
     */
    public function h5Pay(array $orderInfo, string $title = WEB_TITLE): array
    {
        $config   = $this->app->getConfig();
        $data     = [
            'mchid'        => $config->get('mch_id'),
            'appid'        => $config->get('app_id'),
            'description'  => $title . '-vip购买',
            'out_trade_no' => $orderInfo['order_no'],
            'amount'       => ['total' => intval($orderInfo['money'] * 100)],
            'scene_info'   => ['payer_client_ip' => request()->getRealIp(true), 'h5_info' => ['type' => 'Wap']],
            'notify_url'   => self::NOTIFY_URL, // 支付结果通知网址，如果不设置则会使用配置里的默认地址
        ];
        $response = $this->app->getClient()->post('pay/transactions/h5', [
            'json' => $data,
        ]);

        return $response->toArray();
    }

    /**
     * 获取已支付订单信息
     * @param string $order_no
     * @param int    $type
     * @return array
     */
    public function getPayOrderInfo(string $order_no, int $type = 1): array
    {
        $app  = $this->app;
        $data = ['mchid' => (string)$app->getMerchant()->getMerchantId(),];
        try {
            $uri      = $type == 1 ? "pay/transactions/out-trade-no/{$order_no}" : "pay/transactions/id/{$order_no}";
            $response = $app->getClient()->get($uri, [
                'query' => $data,
            ]);
            return $response->toArray();
        } catch (ClientException $exception) {
            return [];
        }

    }

    /**
     * Decrypt AEAD_AES_256_GCM ciphertext
     *
     * @param string $associatedData AES GCM additional authentication data
     * @param string $nonceStr       AES GCM nonce
     * @param string $ciphertext     AES GCM cipher text
     *
     * @return string|bool      Decrypted string on success or FALSE on failure
     */
    public function decryptToString(string $associatedData, string $nonceStr, string $ciphertext): bool|string
    {
        $aesKey     = $this->pay_config['secret_key'];
        $ciphertext = \base64_decode($ciphertext);
        $text       = substr($ciphertext, 0, -self::AUTH_TAG_LENGTH_BYTE);
        $authTag    = substr($ciphertext, -self::AUTH_TAG_LENGTH_BYTE);
        return \openssl_decrypt($text, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr, $authTag, $associatedData);
    }

    /**
     * 支付回调
     * @param $_data
     * @return bool
     */
    public function payCallback($_data): bool
    {
        Log::info(__METHOD__ . ':' . json_encode($_data));
        if (empty($_data['resource'])) return false;
        $response = $this->decryptToString($_data['resource']['associated_data'], $_data['resource']['nonce'], $_data['resource']['ciphertext']);
        $response = json_decode($response, true);
        if ($response['trade_state'] !== 'SUCCESS') return false;

        $order_no = $response['out_trade_no'] ?? '';
        if (empty($order_no)) return false;
        // 使用通知里的 "微信支付订单号" 或者 "商户订单号" 去自己的数据库找到订单
        $order = VipOrder::getOne(['order_no' => $order_no], 'id,uid,order_no,pay_type,vip_type,status');
        if (empty($order) || $order['status'] == PayStatus::PAY_STATUS_SUCCESS->value) return true;     // 告诉微信，我已经处理完了或者订单没找到，别再通知我了

        $uid = $order['uid'];

        // ========== 保险 去微信查询订单信息
        $_order_info = $this->getPayOrderInfo($response['transaction_id'], 2);
        if (empty($_order_info)) return false;
        if ($_order_info['trade_state'] !== 'SUCCESS') return false;
        // ========== 保险 去微信查询订单信息

        try {
            $date_time = date('Y-m-d H:i:s');
            $where[]   = ['order_no', '=', $order_no];
            $data      = [
                'status'   => PayStatus::PAY_STATUS_SUCCESS->value,
                'pay_time' => getBeforeString(str_replace(array('T', 'Z'), ' ', $_order_info['success_time']), '+'),
                'u_time'   => $date_time,
                'trade_no' => $_order_info['transaction_id'],
            ];
            VipOrder::updateDataCache($where, $data);

            return true;    // 返回处理完成
        } catch (\PDOException $exception) {
            Log::error($exception->getMessage());
            return false;   // 让微信再来一次
        }
    }

    protected function getV3Config()
    {
        // 构造一个 APIv3 客户端实例

        // 从本地文件中加载「商户API私钥」，「商户API私钥」会用来生成请求的签名
        $merchantPrivateKeyFilePath = 'file://' . $this->pay_config['private_key'];
        $merchantPrivateKeyInstance = Rsa::from($merchantPrivateKeyFilePath, Rsa::KEY_TYPE_PRIVATE);
        // 从本地文件中加载「微信支付平台证书」，用来验证微信支付应答的签名
        $platformCertificateFilePath = 'file://' . $this->pay_config['cert'];
        $platformPublicKeyInstance   = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        // 从「微信支付平台证书」中获取「证书序列号」
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
        return Builder::factory([
            'mchid'      => $this->pay_config['mch_id'],
            'serial'     => $this->pay_config['certificate_serial_no'],
            'privateKey' => $merchantPrivateKeyInstance,
            'certs'      => [
                $platformCertificateSerial => $platformPublicKeyInstance,
            ],
        ]);
    }

    //        "amount": {
    //            "currency": "CNY",
    //            "payer_currency": "CNY",
    //            "payer_total": 1,
    //            "total": 1
    //        },
    //        "appid": "wx4d001bea4",
    //        "attach": "",
    //        "bank_type": "OTHERS",
    //        "mchid": "16209",
    //        "out_trade_no": "vip10220543wEKnyiRF",
    //        "payer": {
    //            "openid": "ogk"
    //        },
    //        "promotion_detail": [],
    //        "success_time": "2022-05-24T19:33:40+08:00",
    //        "trade_state": "SUCCESS",
    //        "trade_state_desc": "支付成功",
    //        "trade_type": "MWEB",
    //        "transaction_id": "4200539"
    //订单查询
    public function _query(string $order_no)
    {
        $promise = $this->getV3Config()
            ->v3->pay->transactions->outTradeNo->_out_trade_no_
            ->getAsync([
                'query'        => ['mchid' => $this->pay_config['mch_id']],
                'out_trade_no' => $order_no,
            ])->then(static function ($response) {

                return json_decode((string)$response->getBody(), true);
            })
            ->otherwise(static function ($e) {
                $result = ['code' => 0, 'msg' => $e->getMessage(), 'data' => '', 'url' => '', 'wait' => 0,];
                return (json($result));
            });
        return $promise->wait();
    }

    //退款
    //array(11) {
    //  ["amount"]=>
    //  array(9) {
    //    ["currency"]=>
    //    string(3) "CNY"
    //    ["discount_refund"]=>
    //    int(0)
    //    ["from"]=>
    //    array(0) {
    //    }
    //    ["payer_refund"]=>
    //    int(1)
    //    ["payer_total"]=>
    //    int(1)
    //    ["refund"]=>
    //    int(1)
    //    ["settlement_refund"]=>
    //    int(1)
    //    ["settlement_total"]=>
    //    int(1)
    //    ["total"]=>
    //    int(1)
    //  }
    //  ["channel"]=>
    //  string(8) "ORIGINAL"
    //  ["create_time"]=>
    //  string(25) "2022-08-29T23:33:26+08:00"
    //  ["funds_account"]=>
    //  string(9) "AVAILABLE"
    //  ["out_refund_no"]=>
    //  string(31) "CZ166178551jGzXWhxr"
    //  ["out_trade_no"]=>
    //  string(31) "CZ166178556511jGzXWhxr"
    //  ["promotion_detail"]=>
    //  array(0) {
    //  }
    //  ["refund_id"]=>
    //  string(29) "50300203024336680876"
    //  ["status"]=>
    //  string(10) "PROCESSING"
    //  ["transaction_id"]=>
    //  string(28) "42000015323968"
    //  ["user_received_account"]=>
    //  string(18) "支付用户零钱"
    //}
    public function refund(array $params = [])
    {
        $data    = [
            'transaction_id' => $params['transaction_id'],//交易订单号
            'out_refund_no'  => $params['out_trade_no'],//商户单号
            'amount'         => [
                'refund'   => (int)$params['amount']['total'],//单位分
                'total'    => (int)$params['amount']['total'],//单位分
                'currency' => 'CNY',
            ],
        ];
        $promise = $this->getV3Config()
            ->chain('v3/refund/domestic/refunds')
            ->postAsync([
                'json' => $data,
            ])->then(static function ($response) {
                return json_decode((string)$response->getBody(), true);
            })->otherwise(static function ($e) {
//                $result = ['code' => 0, 'msg' => $e->getMessage(), 'data' => '', 'url' => '', 'wait' => 0,];
                return $e->getMessage();
            });

        return $promise->wait();
    }

    /**
     * JSAPI支付
     * https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_4.shtml
     * 统一下单
     * https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
     *
     * @param string $openid     调用【网页授权获取用户信息】接口获取到用户在该公众号下的Openid
     * @param int    $totalFee   收款总费用 单位元
     * @param string $outTradeNo 唯一的订单号
     * @param string $orderName  订单名称
     * @return array
     */
    public function jsapiPay(string $openid, int $totalFee, string $outTradeNo, string $orderName, string $notifyUrl = self::NOTIFY_URL): array
    {
        $noncestr                = getNonce();
        $timeStamp               = time();
        $url                     = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
        $url_parts               = parse_url($url);//解析url
        $canonical_url           = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
        $appid                   = $this->pay_config['app_id'];                                  //appID
        $mchid                   = $this->pay_config['mch_id'];                                  //商户ID
        $serialNo                = $this->pay_config['certificate_serial_no'];                   //证书序列号
        $data                    = [];
        $time                    = $timeStamp;
        $data['appid']           = $appid;
        $data['mchid']           = $mchid;
        $data['description']     = $orderName;                                                //商品描述
        $data['out_trade_no']    = $outTradeNo;                                               //订单编号
        $data['attach']          = 'pay';                                                     //备注
        $data['notify_url']      = $notifyUrl;                                                //回调接口 需根据自己的情况修改
        $data['amount']['total'] = $totalFee;                                                 //金额 单位 分
        $data['payer']['openid'] = $openid;                                                   //用户openID

        $key    = $this->getSignV3(json_encode($data), $canonical_url, $noncestr, $time);//签名
        $token  = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
            $mchid, $noncestr, $timeStamp, $serialNo, $key);
        $header = array(
            'Content-Type:' . 'application/json; charset=UTF-8',
            'Accept:application/json',
            'User-Agent:*/*',
            'Authorization: WECHATPAY2-SHA256-RSA2048 ' . $token
        );
        //获取到prepay_id
        $ret = curlPost($url, $data, $header);
        if ($ret['code'] != 200) return [];
        if (!isset($ret['data']['prepay_id'])) return [];
        $prepayId = $ret['data']['prepay_id'];

        //微信支付（小程序）签名
        $str = $this->getWechartSign($appid, $timeStamp, $noncestr, 'prepay_id=' . $prepayId);
        return [
            'appid'     => $appid,
            'signType'  => 'RSA',
            'nonceStr'  => $noncestr,
            'timeStamp' => "$timeStamp",//方便前端区分数字还是字符串
            'package'   => 'prepay_id=' . $prepayId,
            'paySign'   => $str
        ];
    }

    /**
     * 微信支付签名
     *
     * @param $data
     * @param $url
     * @param $randstr
     * @param $time
     * @return string
     */
    public function getSignV3($data, $url, $randstr, $time)
    {
        $str = "POST" . "\n" . $url . "\n" . $time . "\n" . $randstr . "\n" . $data . "\n";
        $key = file_get_contents($this->pay_config['private_key']);//在商户平台下载的秘钥
        return $this->getSha256WithRSA($str, $key);
    }

    /**
     * @param $content
     * @param $privateKey
     * @return string
     */
    public function getSha256WithRSA($content, $privateKey)
    {
        $binary_signature = "";
        $algo             = "SHA256";
        openssl_sign($content, $binary_signature, $privateKey, $algo);
        return base64_encode($binary_signature);
    }

    /**
     * 调起支付的签名
     *
     * @param $appid
     * @param $timeStamp
     * @param $noncestr
     * @param $prepay_id
     * @return string
     */
    public function getWechartSign($appid, $timeStamp, $noncestr, $prepay_id)
    {
        $str = $appid . "\n" . $timeStamp . "\n" . $noncestr . "\n" . $prepay_id . "\n";
        $key = file_get_contents($this->pay_config['private_key']);
        return $this->getSha256WithRSA($str, $key);
    }

    /**
     * 获取用户的openid
     * 通过跳转获取用户的openid，跳转流程如下：
     * 1、设置自己需要调回的url及其其他参数，跳转到微信服务器https://open.weixin.qq.com/connect/oauth2/authorize
     * 2、微信服务处理完成之后会跳转回用户redirect_uri地址，此时会带上一些参数，如：code
     * @return openid
     */
    public function getOpenid()
    {
        //通过code获得openid
        if (!isset($_GET['code'])) {
            //触发微信返回code码
            $baseUrl = urlencode(self::NOTIFY_URL);
            $url     = $this->createOauthUrlForCode($baseUrl);

            Header("Location: $url");

            return false;
        } else {
            //获取code码，以获取openid
            $code = $_GET['code'];
            return $this->getOpenidFromMp($code);
        }
    }

    /**
     * 通过code从工作平台获取openid机器access_token
     *
     * @param string $code 微信跳转回来带上的code
     * @return openid
     */
    public function getOpenidFromMp($code)
    {
        $url = $this->createOauthUrlForOpenid($code);
        $res = self::curlGet($url);
        //取出openid

        $data       = json_decode($res, true);
        $this->data = $data;
        return $data['openid'];
    }

    /**
     * 构造获取open和access_toke的url地址
     *
     * @param string $code，微信跳转带回的code
     * @return 请求的url
     */
    private function createOauthUrlForOpenid($code)
    {
        $urlObj["appid"]      = $this->pay_config['app_id'];
        $urlObj["secret"]     = $this->pay_config['app_key'];
        $urlObj["code"]       = $code;
        $urlObj["grant_type"] = "authorization_code";
        $bizString            = $this->toUrlParams($urlObj);
        return "https://api.weixin.qq.com/sns/oauth2/access_token?" . $bizString;
    }

    /**
     * 构造获取code的url连接
     *
     * @param string $redirectUrl 微信服务器回跳的url，需要url编码
     * @return 返回构造好的url
     */
    private function createOauthUrlForCode($redirectUrl)
    {
        $urlObj["appid"]            = $this->pay_config['app_id'];
        $urlObj["redirect_uri"]     = "$redirectUrl";
        $urlObj["response_type"]    = "code";
        $urlObj["scope"]            = "snsapi_base";
        $urlObj["connect_redirect"] = "1";
        $urlObj["state"]            = "STATE" . "#wechat_redirect";
        $bizString                  = $this->toUrlParams($urlObj);
        return "https://open.weixin.qq.com/connect/oauth2/authorize?" . $bizString;
    }

    /**
     * 拼接签名字符串
     *
     * @param array $urlObj
     * @return 返回已经拼接好的字符串
     */
    private function toUrlParams($urlObj)
    {
        $buff = "";
        foreach ($urlObj as $k => $v) {
            if ($k != "sign") $buff .= $k . "=" . $v . "&";
        }
        $buff = trim($buff, "&");
        return $buff;
    }

    public static function curlGet($url = '', $options = array())
    {
        $ch = curl_init($url);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        curl_setopt($ch, CURLOPT_TIMEOUT, 60);
        if (!empty($options)) {
            curl_setopt_array($ch, $options);
        }
        //https请求 不验证证书和host
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
        curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
        $data = curl_exec($ch);
        curl_close($ch);
        return $data;
    }
}